/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies, 
 * either expressed or implied, of the CometMI Project.
 */
package org.cometmi.gwt.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import org.cometmi.gwt.shared.MiProxy;
import org.cometmi.gwt.shared.annotations.DeclaredConstructor;
import org.cometmi.gwt.shared.annotations.TargetMiObjectClass;
import org.cometmi.gwt.shared.exceptions.InvalidArgumentException;
import org.cometmi.gwt.shared.request.ptypes.Param;
import org.cometmi.gwt.shared.request.var.ReturnVar;
import org.cometmi.gwt.shared.util.ProxyBase;
import org.cometmi.gwt.shared.util.UtilityFunctions;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.generator.NameFactory;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

class ProxyCreator
{
	/** Simple name of class to be generated */
	private String generatedClassName = null;

	/** Package name of class to be generated */
	private String packageName = null;

	/** The type of provided interface */
	private JClassType interfaceType;

	private String canonicalName;

	/**
	 * Standard constructor: creates a class implementing the provided interface
	 * type which should be sub-type of {@link MiProxy}.
	 * 
	 * @param logger
	 * @param context
	 * @param interfaceType
	 * @throws UnableToCompleteException
	 */
	ProxyCreator(TreeLogger logger, GeneratorContext context, JClassType interfaceType)
			throws UnableToCompleteException
	{
		this.interfaceType = interfaceType;

		this.generatedClassName = this.interfaceType.getSimpleSourceName() + "___Impl";
		this.packageName = this.interfaceType.getPackage().getName();

		this.canonicalName = this.packageName + "." + this.generatedClassName;

		generateClass(logger, context);

		logger.log(TreeLogger.INFO, "Generated class " + this.canonicalName);
	}

	/**
	 * Returns the name of the generated class.
	 * 
	 * @return
	 */
	String getCanonicalName()
	{
		return this.canonicalName;
	}

	/**
	 * Returns the type of the provided interface.
	 * 
	 * @return
	 */
	public JClassType getInterfaceType()
	{
		return this.interfaceType;
	}

	private void generateClass(TreeLogger logger, GeneratorContext context) throws UnableToCompleteException
	{
		TypeOracle typeOracle = context.getTypeOracle();
		PrintWriter printWriter = context.tryCreate(logger, this.packageName, this.generatedClassName);

		/*
		 * When the print writer is null, the source code has ALREADY been
		 * generated, in such case nothing will be done.
		 */
		if (printWriter == null) return;

		/* initialize composer, set class properties, create source writer */
		ClassSourceFileComposerFactory composer = null;
		composer = new ClassSourceFileComposerFactory(this.packageName, this.generatedClassName);

		composer.addImport(ProxyBase.class.getCanonicalName());
		composer.addImport(MiProxy.class.getCanonicalName());
		composer.addImport(List.class.getCanonicalName());
		composer.addImport(ArrayList.class.getCanonicalName());
		composer.addImport(Param.class.getCanonicalName());
		composer.addImport(UtilityFunctions.class.getCanonicalName());

		composer.setSuperclass(ProxyBase.class.getSimpleName());
		composer.addImplementedInterface(this.interfaceType.getQualifiedSourceName());

		SourceWriter sourceWriter = null;
		sourceWriter = composer.createSourceWriter(context, printWriter);

		/* Generating constructor and required methods */
		generateConstructor(logger, sourceWriter, typeOracle);
		generateProxyMethods(logger, sourceWriter, typeOracle);

		/* close generated class */
		sourceWriter.outdent();
		sourceWriter.println("}");

		context.commit(logger, printWriter);
	}

	/**
	 * Generate source code for the default constructor.
	 * 
	 * @param logger
	 * @param w Source writer to output source code.
	 * @param typeOracle
	 * @throws UnableToCompleteException
	 */
	private void generateConstructor(TreeLogger logger, SourceWriter w, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		w.println("public " + this.generatedClassName + "() { ");
		w.indent();

		StringBuilder signature = new StringBuilder();
		DeclaredConstructor a = this.interfaceType.getAnnotation(DeclaredConstructor.class);
		TargetMiObjectClass b = this.interfaceType.getAnnotation(TargetMiObjectClass.class);

		if (a != null)
			for (Class<?> t : a.paramTypes())
				try
				{
					signature.append(typeOracle.getType(t.getCanonicalName()).getJNISignature());
				}
				catch (NotFoundException e)
				{
					logger.log(TreeLogger.ERROR, "Provided parameter type in annotation " + a.getClass() + " in class "
							+ this.interfaceType.getQualifiedSourceName() + "not found", e);
					throw new UnableToCompleteException();
				}

		if (b == null)
		{
			logger.log(TreeLogger.ERROR, "Class " + a.getClass() + " should be annotated with "
					+ TargetMiObjectClass.class.getName());

			throw new UnableToCompleteException();
		}
		else
			w.println("super.targetMiObjectClass = %s.class;", b.miObjectClass().getCanonicalName());

		w.println("super.constrSignature = \"%s\";", signature.toString());

		w.outdent();
		w.println("}");
	}

	protected void generateProxyMethods(TreeLogger logger, SourceWriter w, TypeOracle typeOracle)
			throws UnableToCompleteException
	{
		JMethod[] methods = this.interfaceType.getOverridableMethods();

		for (JMethod method : methods)
			if (method.isPublic()) generateProxyMethod(logger, w, typeOracle, method);
	}

	private void generateProxyMethod(TreeLogger logger, SourceWriter w, TypeOracle typeOracle, JMethod method)
			throws UnableToCompleteException
	{
		NameFactory nameFactory = new NameFactory();

		w.println();

		/* Write the method signature */
		JType returnType = method.getReturnType().getErasedType();

		w.print("public ");
		w.print(returnType.getQualifiedSourceName());
		w.print(" ");
		w.print(method.getName() + "(");

		JParameter[] params = method.getParameters();
		StringBuilder signature = new StringBuilder();
		JClassType retVarClass;
		JClassType miProxyType;

		try
		{
			retVarClass = typeOracle.getType(ReturnVar.class.getCanonicalName());
			miProxyType = typeOracle.getType(MiProxy.class.getCanonicalName());
		}
		catch (NotFoundException e)
		{
			logger.log(TreeLogger.ERROR,
					"Can't find " + ReturnVar.class.getCanonicalName() + " or " + MiProxy.class.getCanonicalName(), e);
			throw new UnableToCompleteException();
		}

		for (int i = 0; i < params.length; ++i)
		{
			if (i > 0) w.print(", ");

			JType paramType = params[i].getType();
			JParameterizedType parametrizedType = paramType.isParameterized();
			JClassType paramProxyType = paramType.isInterface();

			if (parametrizedType != null && retVarClass.isAssignableFrom(parametrizedType))
				/*
				 * If it's parameterized and is a subclass of ReturnVar the
				 * signature is calculated using its parameter.
				 */
				signature.append(parametrizedType.getTypeArgs()[0].getJNISignature());
			else if (paramProxyType != null && miProxyType.isAssignableFrom(paramProxyType))
			{
				/*
				 * If the argument is a proxy, for the signature must be used
				 * the target object type.
				 */
				TargetMiObjectClass anno = paramProxyType.getAnnotation(TargetMiObjectClass.class);

				if (anno != null)
					try
					{
						JClassType targetType = typeOracle.getType(anno.miObjectClass().getCanonicalName());
						signature.append(targetType.getJNISignature());
					}
					catch (NotFoundException e)
					{
						logger.log(TreeLogger.ERROR, "Can't find " + anno.miObjectClass().getCanonicalName(), e);
						throw new UnableToCompleteException();
					}
				else
				{
					logger.log(TreeLogger.ERROR, "Impossible to generated the signature of method " + method.getName()
							+ " because type " + paramProxyType.getQualifiedSourceName()
							+ " has no target object defined.");
					throw new UnableToCompleteException();

				}
			}
			else
				signature.append(paramType.getJNISignature());

			paramType = paramType.getErasedType();

			w.print(paramType.getQualifiedSourceName());
			w.print(" ");

			/* We want to remember the name of parameter to avoid a duplication. */
			String paramName = params[i].getName();
			nameFactory.addName(paramName);

			w.print(paramName);
		}

		w.println(")");

		// Checking throws
		JClassType[] _throws = method.getThrows();
		boolean hasInvalidArgument = false;

		for (int i = 0; i < _throws.length; i++)
		{
			if (i == 0)
				w.print("throws ");
			else
				w.print(", ");

			String exceptionName = _throws[i].getQualifiedSourceName();

			w.print("%s", exceptionName);

			if (exceptionName.equals(InvalidArgumentException.class.getCanonicalName())) hasInvalidArgument = true;
		}

		if (!hasInvalidArgument)
		{
			logger.log(TreeLogger.ERROR,
					"Method " + method.getName() + " of class " + this.interfaceType.getQualifiedSourceName()
							+ " must throws " + InvalidArgumentException.class.getCanonicalName());
			throw new UnableToCompleteException();
		}

		w.println(" {");

		w.indent();

		/* Creating the list */
		String argsList = nameFactory.createName("___args");

		w.println("Object[] %s = new Object[%d];", argsList, params.length);

		for (int i = 0; i < params.length; ++i)
			w.println("%s[%d] = %s;", argsList, i, params[i].getName());

		boolean returnsVoid = (returnType.isPrimitive() == JPrimitiveType.VOID);

		if (!returnsVoid)
		{
			try
			{
				JClassType returnClass = returnType.isClassOrInterface();
				JClassType proxyInt = typeOracle.getType(MiProxy.class.getCanonicalName());

				if (returnClass == null || !proxyInt.isAssignableFrom(returnClass))
				{
					logger.log(
							TreeLogger.ERROR,
							"Return type of " + method.getName() + " of class "
									+ this.interfaceType.getQualifiedSourceName() + " must implement "
									+ MiProxy.class.getCanonicalName());
					throw new UnableToCompleteException();
				}
			}
			catch (NotFoundException e)
			{
				logger.log(TreeLogger.ERROR, "Can't find " + MiProxy.class.getCanonicalName(), e);
				throw new UnableToCompleteException();
			}

			w.print("return (%s)", returnType.getQualifiedSourceName());
		}

		w.print("super.manager.publishMethod(super.objectId, super.targetMiObjectClass, \"%s\", \"%s\", %s",
				method.getName(), signature, argsList);

		/*
		 * In case ther's a not void return type, the proper method from manager
		 * class will be invoked.
		 */
		if (returnsVoid)
			w.println(");");
		else
			w.println(", %s.class);", returnType.getQualifiedSourceName());

		w.outdent();
		w.println("}");

		logger.log(TreeLogger.INFO, "Generated invoker for method " + method.getName() + signature);
	}
}
